JSON.stringify는 순환참조 객체를 지원하지 못하고 예외를 던진다.

문제현상

  1. axios에서 예외가 발생함
  2. 해당 예외 객체를 stringifyError() 메서드에 전달하여 로그로 출력하려고 시도함
  3. 하지만 axios의 에러 객체는 내부적으로 순환참조(circular reference) 를 포함하고 있음
  4. JSON.stringify()는 기본적으로 순환참조가 있는 객체를 직렬화할 수 없기 때문에 예외(TypeError: Converting circular structure to JSON)를 발생시킴
 private stringifyError(error: unknown): string {
    if (error instanceof Error) {
      const details: Record<string, unknown> = {
        name: error.name,
        message: error.message,
        stack: error.stack,
      };
      for (const key of Object.getOwnPropertyNames(error)) {
        if (!(key in details)) {
          // eslint-disable-next-line @typescript-eslint/no-explicit-any
          (details as any)[key] = (error as any)[key];
        }
      }
      return JSON.stringify(details); // <-- 💀
    }
    try {
      return JSON.stringify(error);
    } catch {
      return String(error);
    }
  }

원인분석

written by Chat GPT 4o


해결 방법

1. json-stringify-safe 사용 (추천)

npm install json-stringify-safe
import stringifySafe from 'json-stringify-safe';

return stringifySafe(details);

json-stringify-safe는 순환참조가 감지되면 [Circular]로 대체하여 문자열을 생성함


2. 수동 순환 제거

config, request, response 등을 제거하거나 최소한의 필드만 추출하여 별도 처리

const cleanedError = {
  name: error.name,
  message: error.message,
  stack: error.stack,
  isAxiosError: (error as any).isAxiosError,
  code: (error as any).code,
  url: (error as any)?.config?.url,
  method: (error as any)?.config?.method,
  status: (error as any)?.response?.status,
};
return JSON.stringify(cleanedError);

3. 에러 객체 로깅 전 fallback 처리

try {
  return JSON.stringify(details);
} catch (e) {
  return `Unserializable error object: ${String(error)}`;
}

요약

NodeJS 환경에서 util.inspect 함수를 써도 된다.

역직렬화는 안되지만 콘솔 출력시 객체를 문자열로 변환할때 쓰는 좋은 함수이다.

참조: https://nodejs.org/api/util.html#utilinspectobject-options

위에서 걱정했던 순환참조도 알아서 [Circular]로 감싸준다

const { inspect } = require('node:util');

const obj = {};
obj.a = [obj];
obj.b = {};
obj.b.inner = obj.b;
obj.b.obj = obj;

console.log(inspect(obj));
// <ref *1> {
//   a: [ [Circular *1] ],
//   b: <ref *2> { inner: [Circular *2], obj: [Circular *1] }
// }